﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Package;
using Microsoft.VisualStudio.TextManager.Interop;

namespace BlunderBrain.VisualStudio.AviSynth
{
    /// <summary>
    /// The semantic value type returned from our tokenizer
    /// </summary>
    public partial struct TokenValue
    {
        // Constructors only. The actual fields are defined in AviSynthParser.y.cs
        public TokenValue(TokenInfo info)
        {
            tokenInfo = info;
            sVal = string.Empty;
        }
        
        public TokenValue(TokenInfo info, string value) : this(info)
        {
            sVal = value;
        }
    }

    public partial class AviSynthParser
    {
        /// <summary>
        /// States for parsing a function declaration
        /// </summary>
        private enum FunctionParserStates
        {
            InvalidSyntax = 0,
            ReadFunctionName, 
            ReadStartOfParameters,
            ReadParameterType,
            ReadParameterName,
            EndOfFunction
        }

        private AviSynthTokenizer Tokenizer
        {
            get { return Scanner as AviSynthTokenizer; }
        }

        public AviSynthParser() : base(new AviSynthTokenizer())
        {
#if TRACE_ACTIONS
            string logFile = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ParserTrace.log");
            System.IO.StreamWriter traceLog = new System.IO.StreamWriter(logFile);
            traceLog.AutoFlush = true;
            Console.SetError(traceLog);
#endif
        }

        public bool Parse(AviSynthParseRequest req, AviSynthAuthoringScope scope)
        {
            Tokenizer.SetSource(req.Text, 0);
            Tokenizer.ParseRequest = req;
            return Parse();
        }

        /// <summary>
        /// Adds all functions available for word completion to the specifide AuthoringScope
        /// </summary>
        public void GetDeclarationsForCompletion(AviSynthParseRequest req, AviSynthAuthoringScope scope)
        {
            // find offset and lenght of token that triggered operation
            int tokenStart = GetOffsetForSourceLine(req.Line, req.Text) + req.TokenInfo.StartIndex;
            int tokenLength = req.TokenInfo.EndIndex - req.TokenInfo.StartIndex + 1;

            // Get list of all possible functions
            IEnumerable<AviSynthFunction> functions = req.ScriptEnvironment.InternalFunctions.Union(req.ScriptEnvironment.PluginFunctions).Union(req.ScriptEnvironment.ScriptFunctions);

            // Find all functions whose name starts with the token sorted by name
            IEnumerable<AviSynthFunction> declarations = functions.Where(f => string.Compare(f.Name, 0, req.Text, tokenStart, tokenLength, true) == 0).OrderBy(f => f.Name);
            scope.AddDeclarations(declarations);
        }

        /// <summary>
        /// Makes a quick pass over the whole text referenced in the ParseRequest and tries to detect any function declarations.
        /// Any functions found are stored in the ScriptEnvironment object in the parserequest
        /// </summary>
        public void FindScriptFunctions(AviSynthParseRequest req, AviSynthAuthoringScope scope)
        {
            req.ScriptEnvironment.ScriptFunctions.Clear();
            
            TokenInfo token = new TokenInfo();
            int tokenizerState = 0;
            Tokenizer.SetSource(req.Text, 0);

            while(Tokenizer.ScanTokenAndProvideInfoAboutIt(token, ref tokenizerState))
            {
                if (token.Type == TokenType.Keyword)
                {
                    string tokenName = Tokenizer.GetTokenValue(token);
                    if (string.Compare(tokenName, "function", true) == 0)
                    {
                        AviSynthFunction func = ParseFunctionDeclaration(Tokenizer, ref tokenizerState);
                        if (func != null)
                            req.ScriptEnvironment.ScriptFunctions.Add(func);
                    }
                }
            }
        }


        /// <summary>
        /// Gets an offset for the start the specified line in a stringbuffer
        /// </summary>
        private int GetOffsetForSourceLine(int line, string source)
        {
            int linecount = 0;
            int offset = 0;

            while (linecount < line && offset < source.Length)
                if (source[offset++] == '\n')
                    linecount++;

            return offset;
        }

        /// <summary>
        /// Parses a function declaration into an AviSynthFunction object
        /// </summary>
        /// <param name="tokenizer">Tokenizer positioned after the 'function' keyword</param>
        /// <param name="tokenizerState">State variable for the tokenizer</param>
        /// <returns>An AviSynthFunction object on success, or null if the declaration couldn't be parsed</returns>
        private AviSynthFunction ParseFunctionDeclaration(AviSynthTokenizer tokenizer, ref int tokenizerState)
        {
            FunctionParserStates state = FunctionParserStates.ReadFunctionName;
            TokenInfo token = new TokenInfo();
            string functionName = "";
            string tokenName = "";

            StringBuilder parameterSpec = new StringBuilder();
            List<string> paramNames = new List<string>();

            // Start by finding the function name and then parse the parameter declarations and convert it to a string on the
            // same format used when registering external pluginfunctions. This is then parsed by the AviSynthFunction constructor.
            while (state != FunctionParserStates.EndOfFunction && state != FunctionParserStates.InvalidSyntax)
            {
                // Fetch the next token
                if (tokenizer.ScanTokenAndProvideInfoAboutIt(token, ref tokenizerState))
                {
                    tokenName = tokenizer.GetTokenValue(token);  // Get string representation of token
                    switch (token.Type) // Start examining token type
                    {
                        case TokenType.WhiteSpace:  // Ignore all whitespaces
                            break;
                        case TokenType.Identifier:  // Identifier - only legal as function name or parameter name
                            switch (state)
                            {
                                case FunctionParserStates.ReadFunctionName:
                                    functionName = tokenName;
                                    state  = FunctionParserStates.ReadStartOfParameters;
                                    break;
                                case FunctionParserStates.ReadParameterType:    // Untyped unnamed parameter
                                    parameterSpec.Append('.');
                                    paramNames.Add(tokenName);
                                    break;
                                case FunctionParserStates.ReadParameterName:    // Unnamed parameter
                                    paramNames.Add(tokenName);  // Save the name in the declaration for later since we can't add it via the AviSynthFunction constructor and mark it as non-optional
                                    break;
                                default:
                                    state = FunctionParserStates.InvalidSyntax;
                                    break;
                            }
                            break;
                        case TokenType.Delimiter:   // Delimiter - '('  ')' or ','
                            switch (state)
                            {
                                case FunctionParserStates.ReadStartOfParameters:
                                    if (tokenName == "(")
                                        state = FunctionParserStates.ReadParameterType; // '(' marking start of parameter declarations
                                    else
                                        state = FunctionParserStates.InvalidSyntax;
                                    break;
                                case FunctionParserStates.ReadParameterType:        // ')' marking end of function or ',' marking end of parameter
                                case FunctionParserStates.ReadParameterName:
                                    if (tokenName == ")")
                                        state = FunctionParserStates.EndOfFunction;
                                    else if (tokenName == ",")
                                        state = FunctionParserStates.ReadParameterType;
                                    else
                                        state = FunctionParserStates.InvalidSyntax;
                                    break;
                                default:
                                    state = FunctionParserStates.InvalidSyntax;
                                    break;
                            }
                            break;
                        case TokenType.Keyword:     // Keyword - only types during parameter declaration are legal
                            switch (state)
                            {
                                case FunctionParserStates.ReadParameterType:
                                    switch (tokenName.ToLower())
                                    {
                                        case "clip":
                                            parameterSpec.Append('c');
                                            break;
                                        case "int":
                                            parameterSpec.Append('i');
                                            break;
                                        case "float":
                                            parameterSpec.Append('f');
                                            break;
                                        case "string":
                                            parameterSpec.Append('s');
                                            break;
                                        case "bool":
                                            parameterSpec.Append('b');
                                            break;
                                        case "val":
                                            parameterSpec.Append('.');
                                            break;
                                        default:
                                            state = FunctionParserStates.InvalidSyntax;
                                            break;
                                    }
                                    state = FunctionParserStates.ReadParameterName;
                                    break;
                                default:
                                    state = FunctionParserStates.InvalidSyntax;
                                    break;
                            }
                            break;
                        case TokenType.String:  // String - only legal as a named parameter
                            tokenName = tokenName.Trim(new char[] { '"' });
                            switch (state)
                            {
                                case FunctionParserStates.ReadParameterType:    // Untyped named parameter
                                    parameterSpec.AppendFormat("[{0}]", tokenName);
                                    parameterSpec.Append('.');
                                    paramNames.Add(tokenName);
                                    break;
                                case FunctionParserStates.ReadParameterName:    // Typed named parameter
                                    parameterSpec.Insert(parameterSpec.Length - 1, string.Format("[{0}]", tokenName));
                                    paramNames.Add(tokenName);
                                    break;
                                default:
                                    state = FunctionParserStates.InvalidSyntax;
                                    break;
                            }
                            break;
                        default:        // No other tokentypes legal during function declaration
                            state = FunctionParserStates.InvalidSyntax;
                            break;
                    }
                }
                else if (state != FunctionParserStates.EndOfFunction)
                    state = FunctionParserStates.InvalidSyntax;    // not finished parsing and no more tokens - invalid syntax
            }

            AviSynthFunction func = state == FunctionParserStates.InvalidSyntax ? null :  new AviSynthFunction(functionName, parameterSpec.ToString());
            
            // Add names of the non-optional parameters since we can't do that via the standard AviSynth parameter spec format.
            if (func != null)
            {
                for (int i = 0; i < func.Parameters.Length; i++)
                {
                    if (string.IsNullOrEmpty(func.Parameters[i].Name) && paramNames.Count > i)
                    {
                        func.Parameters[i].Name = paramNames[i];
                        func.Parameters[i].Optional = false;
                    }
                }
            }

            return func;
        }

    }

}
